﻿//-----------------------------------------------------------------------
// <copyright file="CleanCodeRulesAnalyzer.cs" company="None">
//     Copyright (c) 2009-2012
// </copyright>
//-----------------------------------------------------------------------

namespace CleanCode.StyleCopCustomRules
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Diagnostics.CodeAnalysis;
    using StyleCop;
    using StyleCop.CSharp;

    /// <summary>
    /// Implements our own <see cref="SourceAnalyzer"/> for StyleCop simplifying the
    /// walking through the code document.
    /// CleanCodeRulesAnalyzer supports currently C# only.
    /// </summary>
    [SourceAnalyzer(typeof(CsParser))]
    public class CleanCodeRulesAnalyzer : SourceAnalyzer
    {       
        /// <summary>
        /// TokenHandlers collection contains delegates operating 
        /// on the token level (white spaces, keywords, operators etc.)
        /// </summary>
        private readonly List<TokenRuleContainer> tokenHandlers;
        
        /// <summary>
        /// ElementHandlers analyze code document elements (classes, methods, fields etc.) and expressions (for example, variable declarations).
        /// </summary>
        private readonly List<ElementRuleContainer> elementHandlers;

        /// <summary>
        /// ExpressionHandlers analyze code expressions 
        /// </summary>
        private readonly List<ExpressionRuleContainer> expressionHandlers;

        /// <summary>
        /// Initializes a new instance of the <see cref="CleanCodeRulesAnalyzer"/> class.
        /// </summary>
        public CleanCodeRulesAnalyzer()
        {
            this.tokenHandlers = new List<TokenRuleContainer>();
            this.elementHandlers = new List<ElementRuleContainer>();
            this.expressionHandlers = new List<ExpressionRuleContainer>();

            this.AddRules();
        }

        /// <summary>
        /// Gets the settings pages for the customized Clean Code rules.
        /// </summary>
        /// <value>The settings pages for the customized Clean Code rules.</value>
        public override ICollection<IPropertyControlPage> SettingsPages
        {
            get
            {
                return new IPropertyControlPage[] { new CleanCodeSettings(this) };
            }
        }

        /// <summary>
        /// Analyzes a code document.
        /// </summary>
        /// <param name="document">The <see cref="CodeDocument"/> that will be analyzedS.</param>
        public override void AnalyzeDocument(CodeDocument document)
        {
            var csharpDocument = document as CsDocument;
            if ((csharpDocument != null) && (csharpDocument.RootElement != null) && (csharpDocument.RootElement.Generated == false))
            {
                try
                {
                    this.WalkDocumentTokens(csharpDocument);
                    csharpDocument.WalkDocument(this.VisitElement, null, this.VisitExpression, this);
                }
                catch (Exception e)
                {
                    this.HandleException(e);
                }
            }
        }

        /// <summary>
        /// Gets the property value.
        /// </summary>
        /// <typeparam name="T">templated item</typeparam>
        /// <param name="document">The document.</param>
        /// <param name="propertyName">Name of the property.</param>
        /// <returns>Returns the templated type.</returns>
        public T GetPropertyValue<T>(CsDocument document, string propertyName)
        {
            if (document != null)
            {
                var property = GetSetting(document.Settings, propertyName) as PropertyValue<T>;
                if (property != null)
                {
                    return property.Value;
                }

                var descriptor = PropertyDescriptors.GetPropertyDescriptor(propertyName) as PropertyDescriptor<T>;
                if (descriptor != null)
                {
                    return descriptor.DefaultValue;
                }
            }

            return default(T);
        }

        /// <summary>
        /// Gets the enum property.
        /// </summary>
        /// <typeparam name="TEnum">The type of the enum.</typeparam>
        /// <param name="document">The document.</param>
        /// <param name="propertyName">Name of the property.</param>
        /// <returns>Returns TEnum?</returns>
        public TEnum GetEnumProperty<TEnum>(CsDocument document, string propertyName)
            where TEnum : struct
        {
            var text = this.GetPropertyValue<string>(document, propertyName);
            if (string.IsNullOrEmpty(text))
            {
                const int FirstEnumEntry = 0;
                return (TEnum)System.Enum.ToObject(typeof(TEnum), FirstEnumEntry);
            }

            var converter = new EnumConverter(typeof(TEnum));
// ReSharper disable PossibleNullReferenceException
            return (TEnum)converter.ConvertFromString(text);
// ReSharper restore PossibleNullReferenceException
        }

        /// <summary>
        /// Determines whether the specified element should the be checked.
        /// </summary>
        /// <param name="element">The element.</param>
        /// <returns>
        /// Returns true if the element is not null and does not represent generated code.
        /// </returns>
        private static bool ShouldBeChecked(CsElement element)
        {
            return element != null && (element.Generated == false);
        }

        /// <summary>
        /// Handles the exception occured during analyzing a document.
        /// </summary>
        /// <param name="e">The exception that was raised.</param>
        private void HandleException(Exception e)
        {
            this.Log(StyleCopLogLevel.High, e.ToString());
            const int FictiveLineNumber = 1;
            const ICodeElement NullCodeElement = null;
            const string RuleName = "ExceptionOccurred";
// ReSharper disable ExpressionIsAlwaysNull
            this.AddViolation(NullCodeElement, FictiveLineNumber, RuleName, e.GetType(), e.ToString());
// ReSharper restore ExpressionIsAlwaysNull
        }

        /// <summary>
        /// Adds the rules.
        /// </summary>
        private void AddRules()
        {
            this.AddMethodRuleHandler(StaticRules.CheckElementsForFlagParameters);
            this.AddMethodRuleHandler(StaticRules.CheckElementsForOutputParameters);
            this.AddMethodRuleHandler(StaticRules.CheckElementsForSwitchStatement);
            this.AddMethodRuleHandler(StaticRules.CheckElementsForTooLongMethods);
            this.AddMethodRuleHandler(StaticRules.CheckElementsForTooManyParameters);

            this.AddExpressionRuleHandler(StaticRules.CheckExpressionForMagicNumbers);
            this.AddExpressionRuleHandler(StaticRules.CheckExpressionForUnencapsulatedConditionals);
            this.AddExpressionRuleHandler(StaticRules.CheckExpressionForNegativeConditionals);
            this.AddExpressionRuleHandler(StaticRules.CheckExpressionForUnencapsulatedBoundaryConditions);
        }

        /// <summary>
        /// Adds a rule handler for expressions.
        /// </summary>
        /// <param name="ruleCallback">
        /// The rule callback.
        /// </param>
        private void AddExpressionRuleHandler(CodeWalkerExpressionVisitor<CleanCodeRulesAnalyzer> ruleCallback)
        {
            this.expressionHandlers.Add(
                new ExpressionRuleContainer { RuleCallback = ruleCallback });
        }

        /// <summary>
        /// Adds a rule handler for elements of type method.
        /// </summary>
        /// <param name="ruleCallback">
        /// The rule callback.
        /// </param>
        private void AddMethodRuleHandler(CodeWalkerElementVisitor<CleanCodeRulesAnalyzer> ruleCallback)
        {
            this.elementHandlers.Add(
                new ElementRuleContainer
                    { 
                    ElementType = ElementType.Method, RuleCallback = ruleCallback
                });
        }

        /// <summary>
        /// Walks through the document tokens.
        /// </summary>
        /// <param name="document">The C# document.</param>
        private void WalkDocumentTokens(CsDocument document)
        {
            for (var node = document.Tokens.First; node != null; node = node.Next)
            {
                foreach (var tokenRuleContainer in this.tokenHandlers)
                {
                    if (tokenRuleContainer.TokenType == node.Value.CsTokenType)
                    {
                        tokenRuleContainer.RuleCallback(document.RootElement, node, this);
                    }
                }
            }
        }

        /// <summary>
        /// Visits the element according to the visitor pattern of StyleCop's WalkDocument method.
        /// </summary>
        /// <param name="element">The element that is visited.</param>
        /// <param name="parentElement">The parent element of the element that is visited.</param>
        /// <param name="context">The visitor context data, which is our own <see cref="CleanCodeRulesAnalyzer"/>.</param>
        /// <returns>Returns true to continue, or false to stop the walker.</returns>
        private bool VisitElement(CsElement element, CsElement parentElement, CleanCodeRulesAnalyzer context)
        {
            if (ShouldBeChecked(element))
            {
                foreach (var elementRuleContainer in this.elementHandlers)
                {
                    if (elementRuleContainer.ElementType == element.ElementType)
                    {
                        elementRuleContainer.RuleCallback(element, parentElement, context);
                    }
                }
            }

            return true;
        }

        /// <summary>
        /// Visits the expression according to the visitor pattern of StyleCop's WalkDocument method.
        /// </summary>
        /// <param name="expression">The expression that is visited.</param>
        /// <param name="parentExpression">The parent expression.</param>
        /// <param name="parentStatement">The parent statement.</param>
        /// <param name="parentElement">The parent element.</param>
        /// <param name="context">The visitor context data, which is our own <see cref="CleanCodeRulesAnalyzer"/>.</param>
        /// <returns>
        /// Returns true to continue, or false to stop the walker.
        /// </returns>
        [SuppressMessage("CleanCode.StyleCopCustomRules.CleanCodeRulesAnalyzer", "CC4601:TooManyParameters",
            Justification = "It's a StyleCop delegate.")]        
        private bool VisitExpression(Expression expression, Expression parentExpression, Statement parentStatement, CsElement parentElement, CleanCodeRulesAnalyzer context)
        {
            if (expression != null)
            {
                foreach (var expressionRuleContainer in this.expressionHandlers)
                {
                    expressionRuleContainer.RuleCallback(
                        expression,
                        parentExpression,
                        parentStatement,
                        parentElement,
                        context);
                }
            }

            return true;
        }
    }
}
